Skip to main content

tablo_core/schema/
fields.rs

1//! One [`Field`] type whose control is text, choice, file, or an app's own [`Control`], built from
2//! a path binding a column or an embedded leaf.
3
4mod availability;
5mod builders;
6mod check;
7mod choice;
8mod chrome;
9pub(crate) mod custom;
10pub(crate) mod file;
11mod placement;
12mod read_only;
13mod render;
14#[cfg(feature = "rich-text")]
15mod rich_text;
16mod text;
17
18use std::sync::Arc;
19
20pub(crate) use availability::{Disabled, Hidden};
21pub use builders::{
22    ChoiceField, CustomField, FileField, IntoFormValue, IntoOptions, RepeaterField, TextField,
23};
24pub(crate) use check::Items;
25pub(crate) use choice::{ChoiceControl, Relationship, option_view};
26pub(crate) use chrome::{FieldChrome, error_kind, render_field};
27pub(crate) use custom::{Control, ControlInput};
28pub use custom::{Switch, Toggle};
29pub(crate) use file::{FileControl, is_linkable, is_rooted, stored_upload};
30pub(crate) use placement::{Placement, Posting};
31pub(crate) use read_only::{read_only, value_cell};
32#[cfg(feature = "rich-text")]
33use rich_text::RichTextControl;
34#[cfg(feature = "rich-text")]
35pub use rich_text::RichTextField;
36pub(crate) use text::{Kind, Pattern, TextControl, password_reveal};
37use toasty::stmt::{List, Path};
38use topcoat::context::Cx;
39
40use super::{condition::Condition, layouts::Span, lenses::Binding, repeater::RepeaterControl};
41use crate::{
42    Text,
43    schema::FormInput,
44    values::{FieldError, FormScalar, FormValue},
45};
46
47/// One form field binding a lens to the control editing it, offering only that control's modifiers
48/// so a modifier on the wrong control does not compile.
49///
50/// ```rust
51/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
52/// # struct User {
53/// #     #[key] #[auto] id: uuid::Uuid,
54/// #     name: String,
55/// #     email: String,
56/// #     age: i64,
57/// # }
58/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
59/// # struct Post {
60/// #     #[key] #[auto] id: uuid::Uuid,
61/// #     body: String,
62/// #     status: String,
63/// #     author_id: uuid::Uuid,
64/// #     featured: bool,
65/// # }
66/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
67/// # struct Doc { #[key] #[auto] id: uuid::Uuid, path: String, attachments: Vec<String> }
68/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
69/// # struct Author { #[key] #[auto] id: uuid::Uuid, name: String }
70/// # #[derive(Debug, Clone, Copy, PartialEq, Eq, tablo_core::Options)]
71/// # enum Status { Draft, Published }
72/// # struct AuthorResource;
73/// # impl tablo_core::extend::OptionSource for AuthorResource {
74/// #     type Model = Author;
75/// #     fn scoped_query(_cx: &topcoat::context::Cx)
76/// #         -> topcoat::Result<toasty::stmt::Query<toasty::stmt::List<Author>>>
77/// #     {
78/// #         Ok(toasty::stmt::Query::all())
79/// #     }
80/// #     fn label(_cx: &topcoat::context::Cx, author: &Author) -> String {
81/// #         author.name.clone()
82/// #     }
83/// # }
84/// # use tablo_core::{Field, Options, lens};
85/// Field::text(lens!(User.name)).placeholder("Ada Lovelace"); // TextField
86/// Field::text(lens!(User.email)).email().unique();
87/// Field::text(lens!(User.age)); // typed: an `i64` column
88/// Field::text(lens!(Post.body)).multiline(6);
89/// Field::choice(lens!(Post.status)).options(Status::options()); // ChoiceField
90/// Field::choice(lens!(Post.author_id)).relationship::<AuthorResource>();
91/// Field::file(lens!(Doc.path)); // FileField
92/// Field::files(lens!(Doc.attachments)); // FileField over a `Vec<String>`
93/// Field::toggle(lens!(Post.featured)); // CustomField
94/// ```
95///
96/// Options belong to a choice: a text field has no `options` method.
97pub struct Field {
98    binding: Binding,
99    /// The declared label, over the binding's.
100    label: Option<Text>,
101    /// Whether the control renders as required: the panel sets it from the record form, and an
102    /// embedded value's derive from its own fields.
103    required: bool,
104    control: ControlKind,
105    /// The condition showing the field, set by `visible_when`.
106    condition: Option<Condition>,
107    /// How many of its grid's columns the field spans, set by `span` and `span_full`.
108    span: Option<Span>,
109    /// Whether the control is a checkbox, which a condition follows through `checked`.
110    checkbox: bool,
111    /// Whether the checkbox must be checked, set by `accepted`.
112    accepted: bool,
113    /// The help text under the control, set by `help`.
114    help: Option<Text>,
115    /// When the control renders disabled and the server ignores what it posts.
116    disabled: Disabled,
117    /// The condition disabling the control, set by `disabled_when`.
118    disabling: Option<Condition>,
119    /// The forms that render nothing for the field, set by `hidden_on_create` and
120    /// `hidden_on_edit`.
121    hidden: Hidden,
122    /// The create form's initial value, as the control posts it, set by `default`.
123    default: Option<FormValue>,
124}
125
126/// The control a [`Field`] renders, with what only that control declares.
127pub(crate) enum ControlKind {
128    /// A one-line `<input>`, or a `<textarea>` when `rows` is set.
129    Text(TextControl),
130    /// A `<textarea>` of HTML, which the browser edits through a toolbar.
131    #[cfg(feature = "rich-text")]
132    RichText(RichTextControl),
133    /// A `<select>` over static options or a relationship's rows.
134    Choice(ChoiceControl),
135    /// A file input storing the uploaded file's path.
136    File(FileControl),
137    /// An app's control, or the built-in [`Toggle`].
138    Custom(Arc<dyn Control>),
139    /// A row of an item's controls per value of a `#[document]` list.
140    Repeater(RepeaterControl),
141}
142
143impl std::fmt::Debug for Field {
144    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145        let control = match &self.control {
146            ControlKind::Text(_) => "text",
147            #[cfg(feature = "rich-text")]
148            ControlKind::RichText(_) => "rich text",
149            ControlKind::Choice(_) => "choice",
150            ControlKind::File(_) => "file",
151            ControlKind::Custom(_) => "custom",
152            ControlKind::Repeater(_) => "repeater",
153        };
154        f.debug_struct("Field")
155            .field("name", &self.name())
156            .field("label", &self.label())
157            .field("required", &self.required)
158            .field("control", &control)
159            .field("condition", &self.condition)
160            .field("help", &self.help)
161            .field("disabled", &self.disabled)
162            .field("disabling", &self.disabling)
163            .field("hidden", &self.hidden)
164            .field("default", &self.default)
165            .finish()
166    }
167}
168
169impl Field {
170    fn bound(binding: Binding, control: ControlKind) -> Self {
171        Self {
172            binding,
173            label: None,
174            required: false,
175            control,
176            condition: None,
177            span: None,
178            checkbox: false,
179            accepted: false,
180            help: None,
181            disabled: Disabled::Never,
182            disabling: None,
183            hidden: Hidden::default(),
184            default: None,
185        }
186    }
187
188    /// Binds any [`FormScalar`] column, storing the type's own spelling and probing uniqueness
189    /// through the lens.
190    pub fn text<M, T>(lens: impl Into<Path<M, T>>) -> TextField
191    where
192        M: toasty::schema::Model,
193        T: FormScalar + toasty::stmt::IntoExpr<T> + 'static,
194    {
195        let path: Path<M, T> = lens.into();
196        let binding = Binding::of(&path);
197        let control =
198            TextControl::new::<M, T>(path, binding.unique(), binding.unique_with().to_vec());
199        TextField::new(Self::bound(binding, ControlKind::Text(control)))
200    }
201
202    /// The text field `#[derive(EmbeddedForm)]` renders for a leaf.
203    #[doc(hidden)]
204    pub fn embedded_leaf<M, T>(path: Path<M, T>) -> TextField
205    where
206        M: toasty::schema::Model,
207        T: FormScalar,
208    {
209        TextField::new(Self::bound(
210            Binding::of(&path),
211            ControlKind::Text(TextControl::leaf::<T>()),
212        ))
213    }
214
215    /// A choice field over any column, with options from [`options`](ChoiceField::options) or
216    /// [`relationship`](ChoiceField::relationship).
217    pub fn choice<M, T>(lens: impl Into<Path<M, T>>) -> ChoiceField
218    where
219        M: toasty::schema::Model,
220    {
221        ChoiceField::new(Self::bound(
222            Binding::of::<M, T>(&lens.into()),
223            ControlKind::Choice(ChoiceControl::default()),
224        ))
225    }
226
227    /// A file field over a `String` column holding the uploaded path, rendering no `value`
228    /// attribute.
229    pub fn file<M>(lens: impl Into<Path<M, String>>) -> FileField
230    where
231        M: toasty::schema::Model,
232    {
233        FileField::new(Self::bound(
234            Binding::of::<M, String>(&lens.into()),
235            ControlKind::File(FileControl::default()),
236        ))
237    }
238
239    /// A file field over a `Vec<String>` column holding the uploaded paths, in the order the
240    /// form lists them: a record form's `#[form(file)]` control on a `Vec<String>`.
241    ///
242    /// The input takes several files at once, which the submission adds after the stored ones,
243    /// and each stored file has its own remove checkbox and, with JavaScript, buttons moving it:
244    /// the list saves in the order its rows show. A resource's form reads the paths into `key`'s
245    /// list, starting from the record's stored list: only a remove checkbox drops a path.
246    /// A page handling its own post receives each chosen file's name under `key`, each row's path
247    /// under `keep_<key>` in the order the rows post them, and each removed path under
248    /// `clear_<key>`, and checks them against its record itself.
249    /// [`FileField::max_files`] bounds the count; the extensions and size apply to each file.
250    pub fn files<M>(lens: impl Into<Path<M, List<String>>>) -> FileField
251    where
252        M: toasty::schema::Model,
253    {
254        FileField::new(Self::bound(
255            Binding::of::<M, List<String>>(&lens.into()),
256            ControlKind::File(FileControl {
257                multiple: true,
258                ..FileControl::default()
259            }),
260        ))
261    }
262
263    /// A checkbox over a `bool` column that submits `false` when unchecked.
264    pub fn toggle<M>(lens: impl Into<Path<M, bool>>) -> CustomField
265    where
266        M: toasty::schema::Model,
267    {
268        let mut field = Self::custom(lens, Toggle);
269        field.0.checkbox = true;
270        field
271    }
272
273    /// A field over any [`FormScalar`] column, rendered by an app's [`Control`].
274    pub fn custom<M, T>(lens: impl Into<Path<M, T>>, control: impl Control + 'static) -> CustomField
275    where
276        M: toasty::schema::Model,
277        T: FormScalar,
278    {
279        CustomField::new(Self::bound(
280            Binding::of::<M, T>(&lens.into()),
281            ControlKind::Custom(Arc::new(control)),
282        ))
283    }
284
285    /// A row of `T`'s controls per item of a `#[document]` list of `T`, which the browser adds,
286    /// removes and moves: a record form's `#[form(repeat)]` control.
287    ///
288    /// Each row posts its item's keys under `{key}.{row}.`. A resource's form and an action's
289    /// input read them into `key`'s rows, and a page handling its own post reads it with
290    /// [`Schema::read`](crate::Schema::read); the items then parse with
291    /// [`parse_items`](crate::schema::parse_items). [`RepeaterField`] has the rules.
292    pub fn repeater<M, T>(lens: impl Into<Path<M, List<T>>>) -> RepeaterField
293    where
294        M: toasty::schema::Model,
295        T: FormInput,
296    {
297        RepeaterField::new(Self::bound(
298            Binding::of::<M, List<T>>(&lens.into()),
299            ControlKind::Repeater(RepeaterControl::new::<T>()),
300        ))
301    }
302
303    /// A text field posting `name`, a key no column binds: a [`FormInput`](crate::FormInput)
304    /// field. `T` picks the input type, as a column's type does for [`Field::text`].
305    pub fn text_input<T: FormScalar>(name: impl Into<String>) -> TextField {
306        TextField::new(Self::bound(
307            Self::named(name.into()),
308            ControlKind::Text(TextControl::leaf::<T>()),
309        ))
310    }
311
312    /// A choice field posting `name`, a key no column binds, with options from
313    /// [`options`](ChoiceField::options).
314    pub fn choice_input(name: impl Into<String>) -> ChoiceField {
315        ChoiceField::new(Self::bound(
316            Self::named(name.into()),
317            ControlKind::Choice(ChoiceControl::default()),
318        ))
319    }
320
321    /// A checkbox posting `name`, a key no column binds, that submits `false` when unchecked.
322    pub fn toggle_input(name: impl Into<String>) -> CustomField {
323        let mut field = Self::bound(
324            Self::named(name.into()),
325            ControlKind::Custom(Arc::new(Toggle)),
326        );
327        field.checkbox = true;
328        CustomField::new(field)
329    }
330
331    /// A binding for `name`, labelled as a column of that name would be.
332    fn named(name: String) -> Binding {
333        let label = Text::field(&name);
334        Binding::named(name, label)
335    }
336
337    /// The variant control of an embedded enum, never required since an empty submit reads the
338    /// variant from the payload.
339    pub(crate) fn discriminant(binding: Binding, variants: Vec<(String, Text)>) -> Self {
340        let choice = ChoiceControl {
341            discriminant: true,
342            options: variants,
343            ..ChoiceControl::default()
344        };
345        Self::bound(binding, ControlKind::Choice(choice))
346    }
347
348    pub(crate) fn misdeclared(&self) -> Option<crate::DeclarationErrorKind> {
349        self.binding.misdeclared()
350    }
351
352    /// The key the field posts: the storage column its path names.
353    pub fn name(&self) -> &str {
354        self.binding.name()
355    }
356
357    /// The declared label, else the binding's.
358    pub(crate) fn label(&self) -> Text {
359        self.label.clone().unwrap_or_else(|| self.binding.label())
360    }
361
362    /// Whether the field is a choice declaring neither options nor a relationship.
363    pub(crate) fn offers_nothing(&self) -> bool {
364        self.as_choice().is_some_and(ChoiceControl::offers_nothing)
365    }
366
367    /// Whether the field is a masked text control, whose value never renders back.
368    pub(crate) fn is_password(&self) -> bool {
369        matches!(&self.control, ControlKind::Text(text) if text.password)
370    }
371
372    pub(crate) fn as_repeater(&self) -> Option<&RepeaterControl> {
373        match &self.control {
374            ControlKind::Repeater(repeater) => Some(repeater),
375            _ => None,
376        }
377    }
378
379    /// Posts under `prefix`: a repeater row's control, keeping its label.
380    pub(crate) fn prefix(&mut self, prefix: &str) {
381        let label = self.label();
382        self.binding = Binding::named(format!("{prefix}{}", self.name()), label);
383        self.prefix_comparisons(prefix);
384    }
385
386    pub(crate) fn as_choice(&self) -> Option<&ChoiceControl> {
387        match &self.control {
388            ControlKind::Choice(choice) => Some(choice),
389            _ => None,
390        }
391    }
392
393    pub(crate) fn is_file(&self) -> bool {
394        matches!(self.control, ControlKind::File(_))
395    }
396
397    pub(crate) fn is_unique(&self) -> bool {
398        matches!(&self.control, ControlKind::Text(text) if text.unique)
399    }
400
401    /// The fields each unique index covering a unique text field holds besides it.
402    pub(crate) fn unique_with(&self) -> Vec<&[String]> {
403        match &self.control {
404            ControlKind::Text(text) if text.unique => text.unique_with(),
405            _ => Vec::new(),
406        }
407    }
408
409    /// The condition showing the field, if any.
410    pub(crate) fn condition(&self) -> Option<&Condition> {
411        self.condition.as_ref()
412    }
413
414    /// How many of its grid's columns the field spans, if set.
415    pub(crate) fn span(&self) -> Option<Span> {
416        self.span
417    }
418
419    /// Shows the field only while `condition` holds, as `visible_when` does.
420    pub(crate) fn set_condition(&mut self, condition: Condition) {
421        self.condition = Some(condition);
422    }
423
424    /// Whether the control is a checkbox.
425    pub(crate) fn is_checkbox(&self) -> bool {
426        self.checkbox
427    }
428
429    /// Whether the control can post `value`: a checkbox posts `true` or `false`, and a choice over
430    /// static options one of their values. `None` when the control does not say: a text field, a
431    /// relationship, or an app's own control.
432    pub(crate) fn can_post(&self, value: &str) -> Option<bool> {
433        if self.checkbox {
434            return Some(value == "true" || value == "false");
435        }
436        match &self.control {
437            // Its key holds a list, which no condition value spells.
438            ControlKind::Choice(choice) if choice.is_multiple() => Some(false),
439            ControlKind::Choice(choice) if !choice.is_relationship() => {
440                Some(value.is_empty() || choice.label_of(value).is_some())
441            }
442            _ => None,
443        }
444    }
445
446    /// Whether the control could never post `value`: a typed text field's type does not parse
447    /// it, a choice over static options or a checkbox does not offer it, or it is a list where
448    /// the control posts one value, or one value where it posts a list.
449    pub(crate) fn refuses(&self, value: &FormValue) -> bool {
450        match (&self.control, value) {
451            (ControlKind::Choice(choice), FormValue::List(items)) if choice.is_multiple() => {
452                !choice.is_relationship()
453                    && !choice.tags
454                    && items.iter().any(|item| choice.label_of(item).is_none())
455            }
456            (ControlKind::Choice(choice), _) if choice.is_multiple() => true,
457            (ControlKind::Text(text), FormValue::Text(value)) => {
458                !value.trim().is_empty() && !text.parses(value)
459            }
460            (_, FormValue::Text(value)) => self.can_post(value) == Some(false),
461            _ => true,
462        }
463    }
464
465    /// Whether the control renders as required.
466    pub(crate) fn is_required(&self) -> bool {
467        self.required
468    }
469
470    /// The create form's initial value, as the control posts it.
471    pub(crate) fn default_value(&self) -> Option<&FormValue> {
472        self.default.as_ref()
473    }
474
475    /// The validation state and help text the control renders with.
476    fn chrome(
477        &self,
478        cx: &Cx,
479        id: String,
480        error: Option<&FieldError>,
481        fallback: Option<String>,
482    ) -> FieldChrome {
483        let mut chrome = FieldChrome::new(cx, id, error, &self.label(), fallback);
484        chrome.help = self.help.clone();
485        chrome
486    }
487
488    /// Renders the control required when its record-form field has no blank answer.
489    pub(crate) fn set_required(&mut self, required: bool) {
490        self.required = required;
491    }
492
493    /// Whether the bound column stores NULL, which a unique index admits any number of times.
494    pub(crate) fn is_nullable(&self) -> bool {
495        self.binding.nullable()
496    }
497
498    /// Whether two submissions spell the same stored value, as the unique check compares them.
499    pub(crate) fn same_value(&self, a: &str, b: &str) -> bool {
500        match &self.control {
501            ControlKind::Text(text) => text.same_value(a, b),
502            _ => a.trim() == b.trim(),
503        }
504    }
505
506    /// The key of the field whose value narrows a dependent choice's options.
507    pub(crate) fn parent_key(&self) -> Option<&str> {
508        self.as_choice().and_then(ChoiceControl::parent_key)
509    }
510
511    /// Whether a submitted choice matches the options its parent's value `parent` offers; a
512    /// multiple choice checks only the keys its stored value `stored` does not hold.
513    pub(crate) async fn validate_exists(
514        &self,
515        cx: &Cx,
516        value: &FormValue,
517        parent: Option<&str>,
518        stored: Option<&FormValue>,
519    ) -> Vec<crate::Text> {
520        match &self.control {
521            ControlKind::Choice(choice) => {
522                choice
523                    .validate_exists(cx, &self.label(), value, parent, stored)
524                    .await
525            }
526            _ => Vec::new(),
527        }
528    }
529
530    /// Re-checks a relationship choice in the write's transaction; a multiple choice re-checks
531    /// only the keys its stored value `stored` does not hold.
532    pub(crate) async fn recheck(
533        &self,
534        cx: &Cx,
535        value: &FormValue,
536        parent: Option<&str>,
537        stored: Option<&FormValue>,
538        ex: &mut dyn toasty::Executor,
539    ) -> Vec<crate::Text> {
540        match &self.control {
541            ControlKind::Choice(choice) => {
542                choice
543                    .recheck(cx, &self.label(), value, parent, stored, ex)
544                    .await
545            }
546            _ => Vec::new(),
547        }
548    }
549
550    /// A multiple choice's `value` with the stored keys it drops that the user cannot view put
551    /// back, or `None` when it drops none.
552    pub(crate) async fn keep_unseen(
553        &self,
554        cx: &Cx,
555        value: &FormValue,
556        stored: &FormValue,
557        ex: &mut dyn toasty::Executor,
558    ) -> Option<FormValue> {
559        match &self.control {
560            ControlKind::Choice(choice) => choice.keep_unseen(cx, value, stored, ex).await,
561            _ => None,
562        }
563    }
564}
565
566#[cfg(test)]
567mod test_support {
568    pub(super) use crate::test_support::DummyUser;
569}
570
571#[cfg(test)]
572mod tests;