Skip to main content

tablo_core/resource/
soft_delete.rs

1//! Soft deletes: the column [`ResourceDef::soft_delete`](super::ResourceDef::soft_delete) names,
2//! the [`Trashed`] scope every loader reads through, and the list's trash control.
3
4use toasty::stmt::{Expr, Path};
5use topcoat::{context::Cx, view::BoxView};
6
7use crate::{
8    DeclarationErrorKind, Lens,
9    table::filter::{Filter, FilterInput, FilterValue},
10    toasty_compat::model::{self, ModelPath},
11};
12
13/// Which records a query over a soft-deleting resource admits: the live ones, the trashed ones,
14/// or both.
15///
16/// On a resource that declares no [`soft_delete`](super::ResourceDef::soft_delete) column, every
17/// record is live, so `Only` admits none.
18#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
19pub enum Trashed {
20    /// The live records only: what every list, form and record loader reads by default.
21    #[default]
22    Without,
23    /// Live and trashed records alike.
24    With,
25    /// The trashed records only.
26    Only,
27}
28
29impl Trashed {
30    /// Whether this scope admits a record that is, or is not, `trashed`.
31    pub fn admits(self, trashed: bool) -> bool {
32        match self {
33            Self::Without => !trashed,
34            Self::With => true,
35            Self::Only => trashed,
36        }
37    }
38
39    /// The scope the list's trash control selects with `value`, or `None` for a value it does not
40    /// offer.
41    pub(crate) fn from_filter(value: Option<&FilterValue>) -> Option<Self> {
42        match value.map(FilterValue::single) {
43            None => Some(Self::Without),
44            Some(Some(WITH)) => Some(Self::With),
45            Some(Some(ONLY)) => Some(Self::Only),
46            Some(_) => None,
47        }
48    }
49}
50
51/// The column a resource soft-deletes through, as its def declares it.
52pub(crate) struct SoftDelete<M> {
53    path: Path<M, Option<jiff::Timestamp>>,
54    /// Whether a loaded record's column is set.
55    read: fn(&M) -> &Option<jiff::Timestamp>,
56    /// The column, or why the lens names none.
57    field: Result<(usize, String), DeclarationErrorKind>,
58}
59
60impl<M: toasty::schema::Model + Send + Sync + 'static> SoftDelete<M> {
61    pub(crate) fn new(lens: Lens<M, Option<jiff::Timestamp>>) -> Self {
62        let field =
63            model::field::<M>(&ModelPath::of(lens.path())).map(|field| (field.index, field.name));
64        Self {
65            path: lens.path().clone(),
66            read: lens.reader(),
67            field,
68        }
69    }
70}
71
72impl<M> SoftDelete<M> {
73    /// The predicate `trashed` adds to a query, or `None` when it admits every record.
74    pub(crate) fn filter(&self, trashed: Trashed) -> Option<Expr<bool>> {
75        match trashed {
76            Trashed::Without => Some(self.path.clone().is_none()),
77            Trashed::With => None,
78            Trashed::Only => Some(self.path.clone().is_some()),
79        }
80    }
81
82    /// Whether `record` is trashed.
83    pub(crate) fn is_trashed(&self, record: &M) -> bool {
84        (self.read)(record).is_some()
85    }
86
87    /// The column's index in the model, or why the lens names no column.
88    pub(crate) fn field(&self) -> Result<&(usize, String), &DeclarationErrorKind> {
89        self.field.as_ref()
90    }
91}
92
93/// The trash control's filter name and URL values.
94pub(crate) const TRASH_FILTER: &str = "trashed";
95const WITH: &str = "with";
96const ONLY: &str = "only";
97
98/// The list's trash control: a select between the live records, every record and the trashed
99/// ones. It adds no predicate itself: the list's base query reads its value through
100/// [`Trashed::from_filter`], since the default it replaces is a predicate no filter could drop.
101pub(crate) struct TrashFilter;
102
103impl<M> Filter<M> for TrashFilter {
104    fn name(&self) -> &str {
105        TRASH_FILTER
106    }
107
108    fn label(&self) -> crate::Text {
109        crate::Text::message("tablo-trash")
110    }
111
112    /// Nothing a row shows: which records are trashed is what Restore and Delete permanently
113    /// offer anyway.
114    fn reads(&self) -> Option<crate::lens::Reads<M>> {
115        Some(crate::lens::Reads::nothing())
116    }
117
118    fn to_expr(&self, _cx: &Cx, _value: &FilterValue) -> Option<Expr<bool>> {
119        None
120    }
121
122    fn is_noop_value(&self, value: &FilterValue) -> bool {
123        Trashed::from_filter(Some(value)).is_some()
124    }
125
126    fn control<'a>(&self, cx: &'a Cx, input: FilterInput) -> BoxView<'a> {
127        input.select_led_by(
128            cx,
129            &crate::t!(cx, "tablo-trash-without"),
130            vec![
131                (WITH.to_string(), crate::t!(cx, "tablo-trash-with")),
132                (ONLY.to_string(), crate::t!(cx, "tablo-trash-only")),
133            ],
134        )
135    }
136}